Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 5097fb632d0fd4602aed0e09bbf2c7fc7b450a09


Parents : e664741
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-02T09:02:39-05:00

feat(micron-wasm): integrate Micron-Parser-Go WASM support and configuration

Changes
Diff

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 218e9465..71fa008b 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -95,6 +95,7 @@ from meshchatx.src.backend.lxmf_utils import (
is_user_facing_lxmf_payload,
lxmf_fields_are_columba_reaction,
lxmf_sidebar_preview_for_conversation_latest_row,
+ validate_app_extensions_for_lxmf_http_send,
)
from meshchatx.src.backend.map_manager import MAX_EXPORT_TILES, TRANSPARENT_TILE
from meshchatx.src.backend.markdown_renderer import MarkdownRenderer
@@ -2334,6 +2335,7 @@ class ReticulumMeshChat:
on_page_download_failure=on_failure,
on_progress_update=on_progress,
timeout=120,
+ reticulum=getattr(self, "reticulum", None),
)
try:
@@ -8044,7 +8046,6 @@ class ReticulumMeshChat:
or "Anonymous Peer"
)
- # get current hops away
hops = RNS.Transport.hops_to(
bytes.fromhex(announce["destination_hash"]),
)
@@ -9618,10 +9619,11 @@ class ReticulumMeshChat:
)
try:
- bot_id = self.bot_handler.start_bot(
+ bot_id = await asyncio.to_thread(
+ self.bot_handler.start_bot,
template_id,
- name=name,
- bot_id=bot_id,
+ name,
+ bot_id,
)
return web.json_response({"bot_id": bot_id, "success": True})
except Exception as e:
@@ -9642,7 +9644,7 @@ class ReticulumMeshChat:
)
try:
- success = self.bot_handler.stop_bot(bot_id)
+ success = await asyncio.to_thread(self.bot_handler.stop_bot, bot_id)
return web.json_response({"success": success})
except Exception as e:
return web.json_response(
@@ -9662,7 +9664,10 @@ class ReticulumMeshChat:
)
try:
- new_id = self.bot_handler.restart_bot(bot_id)
+ new_id = await asyncio.to_thread(
+ self.bot_handler.restart_bot,
+ bot_id,
+ )
return web.json_response({"bot_id": new_id, "success": True})
except Exception as e:
return web.json_response(
@@ -9682,7 +9687,7 @@ class ReticulumMeshChat:
)
try:
- success = self.bot_handler.delete_bot(bot_id)
+ success = await asyncio.to_thread(self.bot_handler.delete_bot, bot_id)
return web.json_response({"success": success})
except Exception as e:
return web.json_response(
@@ -9701,7 +9706,10 @@ class ReticulumMeshChat:
)
try:
- result = self.bot_handler.read_subprocess_log(bot_id)
+ result = await asyncio.to_thread(
+ self.bot_handler.read_subprocess_log,
+ bot_id,
+ )
return web.json_response(result)
except ValueError as e:
return web.json_response(
@@ -9727,7 +9735,11 @@ class ReticulumMeshChat:
)
try:
- self.bot_handler.update_bot_name(bot_id, name)
+ await asyncio.to_thread(
+ self.bot_handler.update_bot_name,
+ bot_id,
+ name,
+ )
return web.json_response({"success": True})
except ValueError as e:
return web.json_response(
@@ -9752,7 +9764,7 @@ class ReticulumMeshChat:
)
try:
- self.bot_handler.request_announce(bot_id)
+ await asyncio.to_thread(self.bot_handler.request_announce, bot_id)
return web.json_response({"success": True})
except ValueError as e:
return web.json_response(
@@ -10026,7 +10038,27 @@ class ReticulumMeshChat:
status=400,
)
- fields = lm.get("fields") if isinstance(lm.get("fields"), dict) else {}
+ raw_fields = lm.get("fields")
+ fields = dict(raw_fields) if isinstance(raw_fields, dict) else {}
+ app_extensions_payload = fields.pop("app_extensions", None)
+ validated_app_extensions = None
+ if app_extensions_payload is not None:
+ if not isinstance(app_extensions_payload, dict):
+ return web.json_response(
+ {"message": "Invalid app_extensions"},
+ status=400,
+ )
+ try:
+ validated_app_extensions = (
+ validate_app_extensions_for_lxmf_http_send(
+ app_extensions_payload,
+ )
+ )
+ except ValueError:
+ return web.json_response(
+ {"message": "Invalid app_extensions"},
+ status=400,
+ )
image_field = None
audio_field = None
@@ -10107,6 +10139,7 @@ class ReticulumMeshChat:
delivery_method=delivery_method,
reply_to_hash=reply_to_hash,
reply_quoted_content=reply_quoted_content,
+ app_extensions=validated_app_extensions,
)
return web.json_response(
@@ -10341,9 +10374,28 @@ class ReticulumMeshChat:
@routes.get("/api/v1/lxmf-messages/{message_hash}/uri")
async def lxmf_message_uri(request):
"""Build a reticulum:// URI; prefer the router cache over DB-only state."""
- message_hash = request.match_info.get("message_hash")
+ from meshchatx.src.backend.meshchat_utils import (
+ find_lxm_by_content_hash_for_paper_uri,
+ hex_identifier_to_bytes,
+ lxmf_message_try_paper_uri_string,
+ normalized_meshchat_lxmf_message_hash_hex,
+ )
- lxm = self.message_router.get_message(bytes.fromhex(message_hash))
+ raw_hash = request.match_info.get("message_hash")
+ nh = normalized_meshchat_lxmf_message_hash_hex(raw_hash)
+ if not nh:
+ return web.json_response(
+ {"message": "Invalid message hash"},
+ status=400,
+ )
+ hb = hex_identifier_to_bytes(nh)
+ if hb is None:
+ return web.json_response(
+ {"message": "Invalid message hash"},
+ status=400,
+ )
+
+ lxm = find_lxm_by_content_hash_for_paper_uri(self.message_router, hb)
if not lxm:
return web.json_response(
@@ -10353,16 +10405,16 @@ class ReticulumMeshChat:
status=404,
)
- try:
- # change delivery method to paper so as_uri works
- original_method = lxm.method
- lxm.method = LXMF.LXMessage.PAPER
- uri = lxm.as_uri()
- lxm.method = original_method # restore
+ uri, err_detail = lxmf_message_try_paper_uri_string(lxm)
+ if not uri:
+ body = {
+ "message": "Could not serialize this LXMF payload as a Paper URI"
+ }
+ if err_detail:
+ body["detail"] = err_detail
+ return web.json_response(body, status=422)
- return web.json_response({"uri": uri})
- except Exception as e:
- return web.json_response({"message": str(e)}, status=500)
+ return web.json_response({"uri": uri})
# delete lxmf messages for conversation
@routes.delete("/api/v1/lxmf-messages/conversation/{destination_hash}")
@@ -12920,6 +12972,11 @@ class ReticulumMeshChat:
self._parse_bool(data["nomad_render_plaintext_enabled"]),
)
+ if "nomad_micron_wasm_enabled" in data:
+ self.config.nomad_micron_wasm_enabled.set(
+ self._parse_bool(data["nomad_micron_wasm_enabled"]),
+ )
+
if "nomad_default_page_path" in data:
from meshchatx.src.backend.page_node import is_allowed_page_filename
@@ -13601,24 +13658,23 @@ class ReticulumMeshChat:
on_file_download_failure,
on_file_download_progress,
on_phase=on_file_download_phase,
+ reticulum=getattr(self, "reticulum", None),
)
downloader.start_time = time.time()
self.active_downloads[download_id] = downloader
- # notify client download started
- AsyncUtils.run_async(
- client.send_str(
- json.dumps(
- {
- "type": "nomadnet.file.download",
- "download_id": download_id,
- "nomadnet_file_download": {
- "status": "started",
- "destination_hash": destination_hash.hex(),
- "file_path": file_path,
- },
+ # notify client download started (await so phase updates cannot reorder ahead of started)
+ await client.send_str(
+ json.dumps(
+ {
+ "type": "nomadnet.file.download",
+ "download_id": download_id,
+ "nomadnet_file_download": {
+ "status": "started",
+ "destination_hash": destination_hash.hex(),
+ "file_path": file_path,
},
- ),
+ },
),
)
@@ -13795,23 +13851,22 @@ class ReticulumMeshChat:
on_page_download_failure,
on_page_download_progress,
on_phase=on_page_download_phase,
+ reticulum=getattr(self, "reticulum", None),
)
self.active_downloads[download_id] = downloader
- # notify client download started
- AsyncUtils.run_async(
- client.send_str(
- json.dumps(
- {
- "type": "nomadnet.page.download",
- "download_id": download_id,
- "nomadnet_page_download": {
- "status": "started",
- "destination_hash": destination_hash.hex(),
- "page_path": page_path,
- },
+ # notify client download started (await so phase updates cannot reorder ahead of started)
+ await client.send_str(
+ json.dumps(
+ {
+ "type": "nomadnet.page.download",
+ "download_id": download_id,
+ "nomadnet_page_download": {
+ "status": "started",
+ "destination_hash": destination_hash.hex(),
+ "page_path": page_path,
},
- ),
+ },
),
)
@@ -14402,6 +14457,7 @@ class ReticulumMeshChat:
"nomad_render_markdown_enabled": ctx.config.nomad_render_markdown_enabled.get(),
"nomad_render_html_enabled": ctx.config.nomad_render_html_enabled.get(),
"nomad_render_plaintext_enabled": ctx.config.nomad_render_plaintext_enabled.get(),
+ "nomad_micron_wasm_enabled": ctx.config.nomad_micron_wasm_enabled.get(),
"nomad_default_page_path": ctx.config.nomad_default_page_path.get(),
"local_message_auto_delete_enabled": ctx.config.local_message_auto_delete_enabled.get(),
"local_message_auto_delete_value": ctx.config.local_message_auto_delete_value.get(),
@@ -15762,7 +15818,6 @@ class ReticulumMeshChat:
# We cannot replay "old" paths from the app layer — Transport.request_path refreshes discovery.
path_outcome = await self._await_transport_path(destination_hash_bytes)
- # find destination identity from hash
destination_identity = RNS.Identity.recall(destination_hash_bytes)
if destination_identity is None:
# we have to bail out of sending, since we don't have the identity/path yet

diff --git a/meshchatx/src/backend/config_manager.py b/meshchatx/src/backend/config_manager.py
index a17e423b..b3dcd7fb 100644
--- a/meshchatx/src/backend/config_manager.py
+++ b/meshchatx/src/backend/config_manager.py
@@ -460,6 +460,11 @@ class ConfigManager:
"nomad_render_plaintext_enabled",
True,
)
+ self.nomad_micron_wasm_enabled = self.BoolConfig(
+ self,
+ "nomad_micron_wasm_enabled",
+ True,
+ )
self.nomad_default_page_path = self.StringConfig(
self,
"nomad_default_page_path",

diff --git a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
index bce79d15..89d9558c 100644
--- a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
+++ b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
@@ -95,6 +95,62 @@
</span>
</template>
</span>
+ <v-tooltip
+ v-if="nomadBrowserRendererChip && !isLoadingNodePage"
+ location="bottom"
+ :open-on-hover="false"
+ :open-on-focus="false"
+ :open-on-click="true"
+ :interactive="true"
+ max-width="320"
+ content-class="!bg-transparent !p-0 shadow-none"
+ >
+ <template #activator="{ props: tooltipActivatorProps }">
+ <span
+ v-bind="tooltipActivatorProps"
+ class="shrink-0 hidden sm:inline-flex sm:items-center max-w-[7.5rem] md:max-w-[9rem] truncate rounded border border-gray-300 bg-gray-50 px-1.5 py-0.5 text-[10px] font-medium leading-tight text-gray-600 cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-gray-400 dark:focus-visible:ring-blue-400"
+ tabindex="0"
+ role="button"
+ >{{ nomadBrowserRendererChip.label }}</span
+ >
+ </template>
+ <div
+ class="max-w-[min(20rem,85vw)] rounded-lg border border-[var(--mc-border-strong)] bg-[var(--mc-surface)] px-3 py-2 text-xs leading-snug text-[var(--mc-text-secondary)] shadow-lg"
+ >
+ <template v-if="nomadBrowserRendererChip.popoverVariant === 'wasm_active'">
+ <span>{{ $t("nomadnet.renderer_popover_micron_wasm_powered") }}</span>
+ <a
+ class="font-medium text-[var(--mc-text-secondary)] underline underline-offset-2 hover:opacity-90"
+ :href="micronParserGoRepoUrl"
+ target="_blank"
+ rel="noopener noreferrer"
+ @click.stop
+ >{{ $t("settings.nomad_micron_wasm_link_label") }}</a
+ ><span>{{
+ $t("nomadnet.renderer_popover_micron_wasm_active_tail", {
+ version: nomadBrowserRendererChip.micronGoRelease,
+ })
+ }}</span>
+ </template>
+ <template v-else-if="nomadBrowserRendererChip.popoverVariant === 'wasm_pending'">
+ <a
+ class="font-medium text-[var(--mc-text-secondary)] underline underline-offset-2 hover:opacity-90"
+ :href="micronParserGoRepoUrl"
+ target="_blank"
+ rel="noopener noreferrer"
+ @click.stop
+ >{{ $t("settings.nomad_micron_wasm_link_label") }}</a
+ ><span>{{
+ $t("nomadnet.renderer_popover_micron_wasm_pending_tail", {
+ version: nomadBrowserRendererChip.micronGoRelease,
+ })
+ }}</span>
+ </template>
+ <template v-else>
+ {{ nomadBrowserRendererChip.tooltipBody }}
+ </template>
+ </div>
+ </v-tooltip>
</div>
<!-- archive button -->
@@ -460,6 +516,12 @@ import IconButton from "../IconButton.vue";
import DropDownMenu from "../DropDownMenu.vue";
import DropDownMenuItem from "../DropDownMenuItem.vue";
import GlobalState from "../../js/GlobalState";
+import {
+ preloadNomadMicronWasm,
+ invalidateNomadMicronWasmPreload,
+ isMicronWasmBundled,
+} from "../../js/MicronWasmLoader";
+import { VTooltip } from "vuetify/components/VTooltip";
export default {
name: "NomadNetworkPage",
@@ -469,6 +531,7 @@ export default {
IconButton,
DropDownMenu,
DropDownMenuItem,
+ VTooltip,
},
props: {
destinationHash: {
@@ -510,6 +573,7 @@ export default {
nodePagePathHistory: [],
nodePageCache: {},
currentPageDownloadId: null,
+ pendingNomadPageCancelWithoutId: false,
isDownloadingNodeFile: false,
nodeFilePath: null,
@@ -540,6 +604,8 @@ export default {
pathfinderInProgress: false,
pendingLoadLatestArchive: false,
+
+ nomadMicronWasmReady: false,
};
},
computed: {
@@ -547,6 +613,19 @@ export default {
const p = GlobalState.config?.nomad_default_page_path;
return typeof p === "string" && p.startsWith("/page/") ? p : "/page/index.mu";
},
+ nomadMicronWasmFeatureEffective() {
+ return isMicronWasmBundled() && (GlobalState.config || {}).nomad_micron_wasm_enabled === true;
+ },
+ micronParserGoRepoUrl() {
+ return "https://git.quad4.io/Go-Libs/micron-parser-go";
+ },
+ nomadMicronWasmActive() {
+ return (
+ this.nomadMicronWasmFeatureEffective &&
+ this.nomadMicronWasmReady === true &&
+ typeof globalThis.micronConvert === "function"
+ );
+ },
nomadRenderOptions() {
const c = GlobalState.config || {};
return {
@@ -554,8 +633,63 @@ export default {
renderHtml: c.nomad_render_html_enabled !== false,
renderPlaintext: c.nomad_render_plaintext_enabled !== false,
nomadDestinationHash: this.selectedNode?.destination_hash || null,
+ nomad_micron_wasm_use: this.nomadMicronWasmFeatureEffective && this.nomadMicronWasmReady === true,
};
},
+ /**
+ * Active page renderer label for the toolbar chip (.mu uses Micron JS vs WASM).
+ */
+ nomadBrowserRendererChip() {
+ if (!this.selectedNode || !this.nodePagePath) {
+ return null;
+ }
+ if (this.isShowingNodePageSource) {
+ return null;
+ }
+ const [p] = this.nodePagePath.split("`");
+ const pathLower = (p || "").toLowerCase();
+ const micronGoRelease =
+ typeof import.meta.env.VITE_MICRON_PARSER_GO_RELEASE === "string" &&
+ import.meta.env.VITE_MICRON_PARSER_GO_RELEASE.trim() !== ""
+ ? import.meta.env.VITE_MICRON_PARSER_GO_RELEASE.trim()
+ : "\u2014";
+ const plainChip = (labelKey, detailKey, detailParams) => {
+ const detail = detailKey ? this.$t(detailKey, detailParams ?? {}) : "";
+ return {
+ label: this.$t(labelKey),
+ popoverVariant: null,
+ tooltipBody: detail,
+ };
+ };
+ if (pathLower.endsWith(".mu")) {
+ if (this.nomadMicronWasmActive) {
+ return {
+ label: this.$t("nomadnet.renderer_chip_micron_wasm"),
+ popoverVariant: "wasm_active",
+ micronGoRelease,
+ };
+ }
+ const wasmPreferred = this.nomadMicronWasmFeatureEffective;
+ if (wasmPreferred && !this.nomadMicronWasmReady) {
+ return {
+ label: this.$t("nomadnet.renderer_chip_micron_js"),
+ popoverVariant: "wasm_pending",
+ micronGoRelease,
+ };
+ }
+ return plainChip("nomadnet.renderer_chip_micron_js", "nomadnet.renderer_hint_micron_js");
+ }
+ if (pathLower.endsWith(".md")) {
+ return plainChip("nomadnet.renderer_chip_markdown", "nomadnet.renderer_hint_markdown");
+ }
+ if (pathLower.endsWith(".html")) {
+ return plainChip("nomadnet.renderer_chip_html", "nomadnet.renderer_hint_html");
+ }
+ if (pathLower.endsWith(".txt")) {
+ return plainChip("nomadnet.renderer_chip_plaintext", "nomadnet.renderer_hint_plaintext");
+ }
+ return null;
+ },
blockedDestinations() {
return GlobalState.blockedDestinations;
},
@@ -689,6 +823,28 @@ export default {
// listen for websocket messages
WebSocketConnection.on("message", this.onWebsocketMessage);
+ this.$watch(
+ () => GlobalState.config?.nomad_micron_wasm_enabled,
+ async (enabled) => {
+ if (!isMicronWasmBundled()) {
+ this.nomadMicronWasmReady = false;
+ return;
+ }
+ if (!enabled) {
+ this.nomadMicronWasmReady = false;
+ return;
+ }
+ invalidateNomadMicronWasmPreload();
+ this.nomadMicronWasmReady = await preloadNomadMicronWasm();
+ }
+ );
+
+ if (isMicronWasmBundled() && GlobalState.config?.nomad_micron_wasm_enabled === true) {
+ preloadNomadMicronWasm().then((ok) => {
+ this.nomadMicronWasmReady = ok === true;
+ });
+ }
+
// load nomadnetwork node if a destination hash was provided on page load
if (this.destinationHash) {
(async () => {
@@ -878,6 +1034,16 @@ export default {
// handle started status
if (nomadnetPageDownload.status === "started") {
+ if (this.pendingNomadPageCancelWithoutId) {
+ this.pendingNomadPageCancelWithoutId = false;
+ WebSocketConnection.send(
+ JSON.stringify({
+ type: "nomadnet.download.cancel",
+ download_id: downloadId,
+ })
+ );
+ return;
+ }
this.currentPageDownloadId = downloadId;
this.nodePageLoadPhase = "finding_path";
return;
@@ -994,8 +1160,9 @@ export default {
// clear page download if it matches
if (this.currentPageDownloadId === downloadId) {
this.currentPageDownloadId = null;
+ this.pendingNomadPageCancelWithoutId = false;
this.isLoadingNodePage = false;
- this.nodePageContent = "Download cancelled";
+ this.nodePageContent = this.$t("nomadnet.page_download_cancelled");
}
// clear file download if it matches
@@ -1249,6 +1416,8 @@ export default {
// get new sequence for this page load
const seq = ++this.nodePageRequestSequence;
+ this.pendingNomadPageCancelWithoutId = false;
+
// get previous page path
const previousNodePagePath = this.nodePagePath;
@@ -1412,6 +1581,10 @@ export default {
this.partialIdsByKey = idsByKey;
this.partialRefreshByKey = refreshByKey;
+ const micronOpts = {
+ useWasm: this.nomadMicronWasmActive,
+ };
+
const muParser = new MicronParser();
const updatePartialDom = (html, ids) => {
const container = this.$el.querySelector(".nodeContainer");
@@ -1433,7 +1606,7 @@ export default {
path,
fields,
(pageContent) => {
- const html = muParser.convertMicronToHtml(pageContent);
+ const html = muParser.convertMicronToHtml(pageContent, {}, micronOpts);
const ids = this.partialIdsByKey[key];
if (ids) {
updatePartialDom(html, ids);
@@ -1449,7 +1622,7 @@ export default {
const scheduleRefresh = () => {
this.partialRefreshTimers[key] = setTimeout(() => {
this.downloadNomadNetPage(dest, path, fields, (content) => {
- const h = muParser.convertMicronToHtml(content);
+ const h = muParser.convertMicronToHtml(content, {}, micronOpts);
const idList = this.partialIdsByKey[key];
if (idList) {
updatePartialDom(h, idList);
@@ -2061,7 +2234,23 @@ export default {
download_id: this.currentPageDownloadId,
})
);
+ return;
+ }
+ if (!this.isLoadingNodePage) {
+ return;
}
+ const parsed = this.parseNomadnetworkUrl(this.nodePagePath || "");
+ const dh = parsed?.destination_hash || this.selectedNode?.destination_hash;
+ const pathPart = parsed?.path;
+ if (dh && pathPart) {
+ const key = this.getNomadnetPageDownloadCallbackKey(dh, pathPart);
+ delete this.nomadnetPageDownloadCallbacks[key];
+ }
+ this.pendingNomadPageCancelWithoutId = true;
+ this.nodePageRequestSequence += 1;
+ this.isLoadingNodePage = false;
+ this.nodePageLoadPhase = null;
+ this.nodePageContent = this.$t("nomadnet.page_download_cancelled");
},
cancelFileDownload() {
if (this.currentFileDownloadId !== null) {

diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue
index a3a04b96..a5bfdb88 100644
--- a/meshchatx/src/frontend/components/settings/SettingsPage.vue
+++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue
@@ -879,6 +879,28 @@
>
</span>
</label>
+ <label v-if="micronWasmBundledInBuild" class="setting-toggle">
+ <Toggle
+ id="nomad-micron-wasm"
+ v-model="config.nomad_micron_wasm_enabled"
+ @update:model-value="onNomadMicronWasmToggle"
+ />
+ <span class="setting-toggle__label">
+ <span class="setting-toggle__title">{{
+ $t("settings.nomad_micron_wasm_title")
+ }}</span>
+ <span class="setting-toggle__description">
+ {{ $t("settings.nomad_micron_wasm_desc_before_link") }}
+ <a
+ class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline underline-offset-2"
+ href="https://git.quad4.io/Go-Libs/micron-parser-go"
+ target="_blank"
+ rel="noopener noreferrer"
+ >{{ $t("settings.nomad_micron_wasm_link_label") }}</a
+ >{{ $t("settings.nomad_micron_wasm_desc_after_link") }}
+ </span>
+ </span>
+ </label>
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
Default page path (no URL path)
@@ -2504,6 +2526,7 @@ import {
} from "../../js/settings/incomingDeliveryLimit";
import { normalizeRetentionValue } from "../../js/localMessageRetention";
import { matchesSettingSearch, normalizeSearchString } from "../../js/settingsSearchUtils";
+import { isMicronWasmBundled } from "../../js/MicronWasmLoader.js";
export default {
name: "SettingsPage",
@@ -2584,6 +2607,7 @@ export default {
nomad_render_markdown_enabled: true,
nomad_render_html_enabled: true,
nomad_render_plaintext_enabled: true,
+ nomad_micron_wasm_enabled: true,
nomad_default_page_path: "/page/index.mu",
local_message_auto_delete_enabled: false,
local_message_auto_delete_value: 30,
@@ -2728,6 +2752,9 @@ export default {
"markdown",
"HTML",
"plaintext",
+ "WASM",
+ "micron-parser-go",
+ "micron-parser",
"index.mu",
"index.html",
"default page",
@@ -2877,6 +2904,9 @@ export default {
};
},
computed: {
+ micronWasmBundledInBuild() {
+ return isMicronWasmBundled();
+ },
settingsSearchActive() {
return normalizeSearchString(this.searchQuery).length > 0;
},
@@ -3546,6 +3576,20 @@ export default {
null
);
},
+ async onNomadMicronWasmToggle(value) {
+ const prev = this.config.nomad_micron_wasm_enabled;
+ this.config.nomad_micron_wasm_enabled = value;
+ try {
+ const newConfig = await patchServerConfig({ nomad_micron_wasm_enabled: value }, window.api);
+ this.config = newConfig;
+ normalizeConfigColors(this.config);
+ this.syncLxmfTransferLimitInputs();
+ } catch (e) {
+ this.config.nomad_micron_wasm_enabled = prev;
+ ToastUtils.error(this.$t("common.save_failed"));
+ console.log(e);
+ }
+ },
async onNomadDefaultPagePathChange() {
await this.updateConfig(
{

diff --git a/meshchatx/src/frontend/js/GlobalState.js b/meshchatx/src/frontend/js/GlobalState.js
index 34a3a35f..b8e475b8 100644
--- a/meshchatx/src/frontend/js/GlobalState.js
+++ b/meshchatx/src/frontend/js/GlobalState.js
@@ -24,6 +24,7 @@ const globalState = reactive({
nomad_render_markdown_enabled: true,
nomad_render_html_enabled: true,
nomad_render_plaintext_enabled: true,
+ nomad_micron_wasm_enabled: true,
nomad_default_page_path: "/page/index.mu",
ui_transparency: 0,
ui_glass_enabled: true,

diff --git a/meshchatx/src/frontend/js/MicronParser.js b/meshchatx/src/frontend/js/MicronParser.js
index 1611b231..33871017 100644
--- a/meshchatx/src/frontend/js/MicronParser.js
+++ b/meshchatx/src/frontend/js/MicronParser.js
@@ -99,6 +99,59 @@ export default class MicronParser extends BaseMicronParser {
});
}
+ static sanitizeRenderedMicronHtml(html) {
+ if (html == null) {
+ return "";
+ }
+ const s = typeof html === "string" ? html : String(html);
+ try {
+ const sanitized = DOMPurify.sanitize(s, {
+ USE_PROFILES: { html: true },
+ ALLOWED_URI_REGEXP,
+ });
+ try {
+ return MicronParser.stripOverlayStyles(sanitized);
+ } catch (e) {
+ console.warn("MicronParser: stripOverlayStyles failed", e);
+ return sanitized;
+ }
+ } catch (error) {
+ console.warn(
+ "DOMPurify is not installed or sanitization failed. Include dompurify or check the build.",
+ error
+ );
+ return `<p style="color: red;">DOMPurify is not installed or sanitization failed.</p>`;
+ }
+ }
+
+ /**
+ * Split Micron source into blocks for WASM conversion, keeping MeshChat partial-include lines on the JS path.
+ */
+ static splitMicronMarkupWasmSegments(markup) {
+ if (markup == null) {
+ return [];
+ }
+ const lines = String(markup).split("\n");
+ const segments = [];
+ let buf = [];
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (MicronParser.PARTIAL_LINE_REGEX.test(trimmed)) {
+ if (buf.length) {
+ segments.push({ type: "mu", text: buf.join("\n") });
+ buf = [];
+ }
+ segments.push({ type: "partial", line });
+ } else {
+ buf.push(line);
+ }
+ }
+ if (buf.length) {
+ segments.push({ type: "mu", text: buf.join("\n") });
+ }
+ return segments;
+ }
+
injectMonospaceStyles() {
if (document.getElementById("micron-monospace-styles")) {
return;
@@ -156,10 +209,23 @@ export default class MicronParser extends BaseMicronParser {
document.head.appendChild(styleEl);
}
- convertMicronToHtml(markup, partialContents = {}) {
- if (markup == null) return "";
- if (typeof markup !== "string") markup = String(markup);
+ convertMicronToHtmlWasmHybrid(markup, partialContents = {}) {
+ const mc = globalThis.micronConvert;
+ const segments = MicronParser.splitMicronMarkupWasmSegments(markup);
+ let html = "";
+ for (const seg of segments) {
+ if (seg.type === "mu") {
+ html += MicronParser.sanitizeRenderedMicronHtml(
+ mc(seg.text, this.darkTheme, this.enableForceMonospace)
+ );
+ } else {
+ html += this._convertMicronToHtmlJs(seg.line + "\n", partialContents);
+ }
+ }
+ return html;
+ }
+ _convertMicronToHtmlJs(markup, partialContents = {}) {
const build = () => {
let html = "";
@@ -230,24 +296,7 @@ export default class MicronParser extends BaseMicronParser {
}
}
- try {
- const sanitized = DOMPurify.sanitize(html, {
- USE_PROFILES: { html: true },
- ALLOWED_URI_REGEXP,
- });
- try {
- return MicronParser.stripOverlayStyles(sanitized);
- } catch (e) {
- console.warn("MicronParser: stripOverlayStyles failed", e);
- return sanitized;
- }
- } catch (error) {
- console.warn(
- "DOMPurify is not installed or sanitization failed. Include dompurify or check the build.",
- error
- );
- return `<p style="color: red;">DOMPurify is not installed or sanitization failed.</p>`;
- }
+ return MicronParser.sanitizeRenderedMicronHtml(html);
};
try {
@@ -269,6 +318,22 @@ export default class MicronParser extends BaseMicronParser {
}
}
+ convertMicronToHtml(markup, partialContents = {}, options = {}) {
+ if (markup == null) return "";
+ if (typeof markup !== "string") markup = String(markup);
+
+ const wantWasm = options.useWasm === true && typeof globalThis.micronConvert === "function";
+ if (wantWasm) {
+ try {
+ return this.convertMicronToHtmlWasmHybrid(markup, partialContents);
+ } catch (e) {
+ console.warn("MicronParser: WASM Micron conversion failed, using JS parser", e);
+ }
+ }
+
+ return this._convertMicronToHtmlJs(markup, partialContents);
+ }
+
convertMicronToFragment(markup) {
if (markup == null) {
return document.createDocumentFragment();

diff --git a/meshchatx/src/frontend/js/MicronWasmLoader.js b/meshchatx/src/frontend/js/MicronWasmLoader.js
new file mode 100644
index 00000000..627722fb
--- /dev/null
+++ b/meshchatx/src/frontend/js/MicronWasmLoader.js
@@ -0,0 +1,94 @@
+/**
+ * Lazy-load micron-parser-go WASM (see https://github.com/Quad4-Software/Micron-Parser-Go ).
+ * Requires wasm_exec.js from Go and micron-parser-go.wasm under /vendor/micron-parser-go/.
+ * Files are fetched at production build time (scripts/fetch-micron-wasm.mjs); omitted builds set VITE_MICRON_WASM_BUNDLED=false.
+ */
+
+let resolvedPromise = null;
+
+/** True when WASM artifacts were present at Vite build time (not runtime probing). */
+export function isMicronWasmBundled() {
+ if (typeof globalThis !== "undefined" && typeof globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__ === "boolean") {
+ return globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__;
+ }
+ return import.meta.env.VITE_MICRON_WASM_BUNDLED === "true";
+}
+
+function baseUrl() {
+ const root = import.meta.env.BASE_URL || "/";
+ return `${root.replace(/\/?$/, "/")}vendor/micron-parser-go`;
+}
+
+function injectScript(src) {
+ const id = "meshchatx-micron-wasm-exec";
+ if (document.getElementById(id)) {
+ return Promise.resolve();
+ }
+ return new Promise((resolve, reject) => {
+ const s = document.createElement("script");
+ s.id = id;
+ s.async = true;
+ s.src = src;
+ s.onload = () => resolve();
+ s.onerror = () => reject(new Error(`Micron WASM: failed to load script ${src}`));
+ document.head.appendChild(s);
+ });
+}
+
+async function instantiateOnce() {
+ if (typeof WebAssembly === "undefined") {
+ throw new Error("Micron WASM: WebAssembly is not available");
+ }
+ const root = baseUrl();
+ await injectScript(`${root}/wasm_exec.js`);
+ if (typeof globalThis.Go === "undefined") {
+ throw new Error("Micron WASM: Go runtime missing after wasm_exec.js load");
+ }
+ const wasmUrl = `${root}/micron-parser-go.wasm`;
+ const go = new globalThis.Go();
+ let result;
+ try {
+ result = await WebAssembly.instantiateStreaming(fetch(wasmUrl), go.importObject);
+ } catch {
+ const buf = await fetch(wasmUrl).then((r) => {
+ if (!r.ok) {
+ throw new Error(`Micron WASM: fetch failed (${r.status})`);
+ }
+ return r.arrayBuffer();
+ });
+ result = await WebAssembly.instantiate(buf, go.importObject);
+ }
+ go.run(result.instance);
+ if (typeof globalThis.micronConvert !== "function") {
+ throw new Error("Micron WASM: micronConvert was not registered");
+ }
+}
+
+export function invalidateNomadMicronWasmPreload() {
+ resolvedPromise = null;
+}
+
+/**
+ * Ensures micron-parser-go WASM is initialized; resolves true when micronConvert is callable.
+ */
+export function preloadNomadMicronWasm() {
+ if (!isMicronWasmBundled()) {
+ return Promise.resolve(false);
+ }
+ if (typeof globalThis.micronConvert === "function") {
+ return Promise.resolve(true);
+ }
+ if (resolvedPromise === null) {
+ resolvedPromise = (async () => {
+ try {
+ await instantiateOnce();
+ return typeof globalThis.micronConvert === "function";
+ } catch (e) {
+ console.warn(e);
+ resolvedPromise = null;
+ return false;
+ }
+ })();
+ }
+ return resolvedPromise;
+}

diff --git a/meshchatx/src/frontend/js/NomadPageRenderer.js b/meshchatx/src/frontend/js/NomadPageRenderer.js
index 8c282e00..11d8f365 100644
--- a/meshchatx/src/frontend/js/NomadPageRenderer.js
+++ b/meshchatx/src/frontend/js/NomadPageRenderer.js
@@ -286,10 +286,13 @@ export function renderNomadPageByPath(
const renderPlaintext = renderOptions.renderPlaintext !== false;
const nomadDestinationHash = renderOptions.nomadDestinationHash || null;
const linkOpts = { destinationHash: nomadDestinationHash };
+ const micronOpts = {
+ useWasm: renderOptions.nomad_micron_wasm_use === true && typeof globalThis.micronConvert === "function",
+ };
const p = (pagePathWithoutData || "").toLowerCase();
if (p.endsWith(".mu")) {
const muParser = new MicronParserClass();
- let out = muParser.convertMicronToHtml(content, pagePartials);
+ let out = muParser.convertMicronToHtml(content, pagePartials, micronOpts);
if (nomadDestinationHash) {
out = isolateNomadLinksInHtml(out, nomadDestinationHash);
}

diff --git a/package.json b/package.json
index f68a09b5..b5e42fa8 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"scripts": {
"dev": "vite dev",
"watch": "pnpm run build-frontend -- --watch",
+ "prebuild-frontend": "node scripts/fetch-micron-wasm.mjs",
"build-frontend": "vite build",
"build-backend": "node scripts/build-backend.js",
"build-docs": "python3 scripts/build/fetch_reticulum_manual.py",

diff --git a/scripts/fetch-micron-wasm.mjs b/scripts/fetch-micron-wasm.mjs
new file mode 100644
index 00000000..48978dac
--- /dev/null
+++ b/scripts/fetch-micron-wasm.mjs
@@ -0,0 +1,72 @@
+#!/usr/bin/env node
+/**
+ * Downloads micron-parser-go WASM release assets and matching wasm_exec.js for Vite public/.
+ * Safe to run offline: exits 0 without files when MICRON_WASM_SKIP=1 or network fails.
+ *
+ * Override URLs:
+ * MICRON_PARSER_GO_WASM_URL
+ * MICRON_GO_WASM_EXEC_URL
+ */
+import fs from "fs";
+import path from "path";
+import { MICRON_PARSER_GO_RELEASE_TAG } from "./micron-parser-go-version.mjs";
+import { micronWasmVendorPaths, micronWasmRepoRoot } from "./micron-wasm-resolve-bundled.mjs";
+
+const DEFAULT_WASM_URL = `https://github.com/Quad4-Software/Micron-Parser-Go/releases/download/${MICRON_PARSER_GO_RELEASE_TAG}/micron-parser-go.wasm`;
+const DEFAULT_WASM_EXEC_URL = "https://raw.githubusercontent.com/golang/go/go1.26.2/lib/wasm/wasm_exec.js";
+
+const TIMEOUT_MS = Number(process.env.MICRON_WASM_FETCH_TIMEOUT_MS || 120000);
+
+function rmQuiet(p) {
+ try {
+ fs.rmSync(p, { force: true });
+ } catch {
+ /* ignore */
+ }
+}
+
+async function fetchBinary(url, destFile) {
+ const ctrl = new AbortController();
+ const t = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
+ try {
+ const res = await fetch(url, { signal: ctrl.signal });
+ if (!res.ok) {
+ throw new Error(`HTTP ${res.status}`);
+ }
+ const buf = Buffer.from(await res.arrayBuffer());
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
+ fs.writeFileSync(destFile, buf);
+ return buf.length;
+ } finally {
+ clearTimeout(t);
+ }
+}
+
+async function main() {
+ if (process.env.MICRON_WASM_SKIP === "1") {
+ console.log("fetch-micron-wasm: MICRON_WASM_SKIP=1, skipping.");
+ process.exit(0);
+ }
+
+ const root = micronWasmRepoRoot();
+ const { dir, wasm, wasmExec } = micronWasmVendorPaths(root);
+ const wasmUrl = process.env.MICRON_PARSER_GO_WASM_URL || DEFAULT_WASM_URL;
+ const execUrl = process.env.MICRON_GO_WASM_EXEC_URL || DEFAULT_WASM_EXEC_URL;
+
+ fs.mkdirSync(dir, { recursive: true });
+
+ try {
+ console.log("fetch-micron-wasm: downloading wasm_exec.js...");
+ await fetchBinary(execUrl, wasmExec);
+ console.log("fetch-micron-wasm: downloading micron-parser-go.wasm...");
+ const n = await fetchBinary(wasmUrl, wasm);
+ console.log(`fetch-micron-wasm: OK (${n} bytes WASM)`);
+ } catch (e) {
+ console.warn("fetch-micron-wasm: failed:", e?.message || e);
+ rmQuiet(wasm);
+ rmQuiet(wasmExec);
+ process.exit(0);
+ }
+}
+
+main();

diff --git a/scripts/micron-parser-go-version.mjs b/scripts/micron-parser-go-version.mjs
new file mode 100644
index 00000000..2043fe9f
--- /dev/null
+++ b/scripts/micron-parser-go-version.mjs
@@ -0,0 +1,5 @@
+/**
+ * Micron-Parser-Go release tag for default WASM URL (scripts/fetch-micron-wasm.mjs)
+ * and build-time tooltip copy (VITE_MICRON_PARSER_GO_RELEASE).
+ */
+export const MICRON_PARSER_GO_RELEASE_TAG = "v1.0.2";

diff --git a/scripts/micron-wasm-resolve-bundled.mjs b/scripts/micron-wasm-resolve-bundled.mjs
new file mode 100644
index 00000000..31dc06f2
--- /dev/null
+++ b/scripts/micron-wasm-resolve-bundled.mjs
@@ -0,0 +1,39 @@
+/**
+ * Resolve paths and detect whether micron-parser-go WASM artifacts are present for Vite/Vitest.
+ */
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+/** Repository root (parent of scripts/). */
+export function micronWasmRepoRoot() {
+ return path.join(__dirname, "..");
+}
+
+export function micronWasmVendorPaths(repoRoot = micronWasmRepoRoot()) {
+ const dir = path.join(repoRoot, "meshchatx", "src", "frontend", "public", "vendor", "micron-parser-go");
+ return {
+ dir,
+ wasm: path.join(dir, "micron-parser-go.wasm"),
+ wasmExec: path.join(dir, "wasm_exec.js"),
+ };
+}
+
+/**
+ * True when both wasm_exec.js and a minimally-sized WASM binary exist (build-time fetch succeeded).
+ */
+export function isMicronWasmBundled(repoRoot = micronWasmRepoRoot()) {
+ const { wasm, wasmExec } = micronWasmVendorPaths(repoRoot);
+ try {
+ if (!fs.existsSync(wasm) || !fs.existsSync(wasmExec)) {
+ return false;
+ }
+ const wasmStat = fs.statSync(wasm);
+ const execStat = fs.statSync(wasmExec);
+ return wasmStat.size >= 8192 && execStat.size >= 1024;
+ } catch {
+ return false;
+ }
+}

diff --git a/tests/frontend/MicronParser.test.js b/tests/frontend/MicronParser.test.js
index 2dd64604..4bcb0d0a 100644
--- a/tests/frontend/MicronParser.test.js
+++ b/tests/frontend/MicronParser.test.js
@@ -707,4 +707,43 @@ describe("MicronParser.js", () => {
expect(frag.childNodes.length).toBeGreaterThan(0);
});
});
+
+ describe("micron-parser-go WASM integration helpers", () => {
+ afterEach(() => {
+ delete globalThis.micronConvert;
+ });
+
+ it("splitMicronMarkupWasmSegments isolates MeshChat partial lines", () => {
+ const dest = "a".repeat(32);
+ const partialLine = `\`{${dest}:/page/partial.mu}`;
+ const segments = MicronParser.splitMicronMarkupWasmSegments(`> hello\nworld\n${partialLine}\ntrailer`);
+ expect(segments).toEqual([
+ { type: "mu", text: "> hello\nworld" },
+ { type: "partial", line: partialLine },
+ { type: "mu", text: "trailer" },
+ ]);
+ });
+
+ it("convertMicronToHtml falls back to JS when WASM throws", () => {
+ globalThis.micronConvert = vi.fn(() => {
+ throw new Error("forced wasm failure");
+ });
+ const p = new MicronParser(true, false);
+ const html = p.convertMicronToHtml("`!ok`!", {}, { useWasm: true });
+ expect(html.length).toBeGreaterThan(0);
+ expect(html.toLowerCase()).toContain("ok");
+ });
+
+ it("convertMicronToHtml stitches WASM segments with JS partial lines", () => {
+ globalThis.micronConvert = vi.fn(() => '<p data-wasm="1">wasm-body</p>');
+ const dest = "b".repeat(32);
+ const partialLine = `\`{${dest}:/page/inc.mu}`;
+ const p = new MicronParser(true, false);
+ const html = p.convertMicronToHtml(`intro line\n${partialLine}`, {}, { useWasm: true });
+ expect(globalThis.micronConvert).toHaveBeenCalled();
+ expect(html).toContain("wasm-body");
+ expect(html).toContain("mu-partial");
+ expect(html).toContain("data-dest");
+ });
+ });
});

diff --git a/tests/frontend/MicronWasmLoader.test.js b/tests/frontend/MicronWasmLoader.test.js
new file mode 100644
index 00000000..001ccb4a
--- /dev/null
+++ b/tests/frontend/MicronWasmLoader.test.js
@@ -0,0 +1,167 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import {
+ invalidateNomadMicronWasmPreload,
+ isMicronWasmBundled,
+ preloadNomadMicronWasm,
+} from "../../meshchatx/src/frontend/js/MicronWasmLoader.js";
+
+describe("MicronWasmLoader.js", () => {
+ let origBundledFlag;
+
+ beforeEach(() => {
+ origBundledFlag = globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__;
+ invalidateNomadMicronWasmPreload();
+ delete globalThis.micronConvert;
+ delete globalThis.Go;
+ document.getElementById("meshchatx-micron-wasm-exec")?.remove();
+ });
+
+ afterEach(() => {
+ invalidateNomadMicronWasmPreload();
+ delete globalThis.micronConvert;
+ delete globalThis.Go;
+ document.getElementById("meshchatx-micron-wasm-exec")?.remove();
+ vi.restoreAllMocks();
+ vi.unstubAllGlobals();
+ if (origBundledFlag === undefined) {
+ delete globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__;
+ } else {
+ globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__ = origBundledFlag;
+ }
+ });
+
+ it("isMicronWasmBundled honors __MESHCHATX_TEST_MICRON_WASM_BUNDLED__", () => {
+ globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__ = true;
+ expect(isMicronWasmBundled()).toBe(true);
+ globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__ = false;
+ expect(isMicronWasmBundled()).toBe(false);
+ });
+
+ it("preloadNomadMicronWasm resolves false without bundling and does not fetch", async () => {
+ globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__ = false;
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response());
+ const ok = await preloadNomadMicronWasm();
+ expect(ok).toBe(false);
+ expect(fetchSpy).not.toHaveBeenCalled();
+ });
+
+ it("preloadNomadMicronWasm resolves false when WebAssembly is unavailable", async () => {
+ globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__ = true;
+ vi.stubGlobal("WebAssembly", undefined);
+ const ok = await preloadNomadMicronWasm();
+ expect(ok).toBe(false);
+ });
+
+ it("preloadNomadMicronWasm resolves false when wasm_exec script fails to load", async () => {
+ globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__ = true;
+ const appendSpy = vi.spyOn(document.head, "appendChild").mockImplementation((node) => {
+ if (node?.tagName === "SCRIPT" && typeof node.onerror === "function") {
+ queueMicrotask(() => node.onerror());
+ }
+ return node;
+ });
+ try {
+ const ok = await preloadNomadMicronWasm();
+ expect(ok).toBe(false);
+ } finally {
+ appendSpy.mockRestore();
+ }
+ });
+
+ it("preloadNomadMicronWasm resolves false when wasm_exec loads but Go is missing", async () => {
+ globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__ = true;
+ const appendSpy = vi.spyOn(document.head, "appendChild").mockImplementation((node) => {
+ if (node?.tagName === "SCRIPT" && typeof node.onload === "function") {
+ queueMicrotask(() => node.onload());
+ }
+ return node;
+ });
+ try {
+ const ok = await preloadNomadMicronWasm();
+ expect(ok).toBe(false);
+ } finally {
+ appendSpy.mockRestore();
+ }
+ });
+
+ it("preloadNomadMicronWasm resolves false when WASM instantiation fails", async () => {
+ globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__ = true;
+ globalThis.Go = class {
+ constructor() {
+ this.importObject = {};
+ this.run = vi.fn();
+ }
+ };
+
+ const appendSpy = vi.spyOn(document.head, "appendChild").mockImplementation((node) => {
+ if (node?.tagName === "SCRIPT" && typeof node.onload === "function") {
+ queueMicrotask(() => node.onload());
+ }
+ return node;
+ });
+
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: true,
+ arrayBuffer: async () => new ArrayBuffer(16),
+ headers: new Headers({ "content-type": "application/wasm" }),
+ });
+
+ const streaming = vi
+ .spyOn(WebAssembly, "instantiateStreaming")
+ .mockRejectedValue(new Error("streaming failed"));
+ const instantiate = vi.spyOn(WebAssembly, "instantiate").mockRejectedValue(new Error("bad wasm"));
+
+ try {
+ const ok = await preloadNomadMicronWasm();
+ expect(ok).toBe(false);
+ expect(streaming).toHaveBeenCalled();
+ expect(instantiate).toHaveBeenCalled();
+ } finally {
+ appendSpy.mockRestore();
+ }
+ });
+
+ it("preloadNomadMicronWasm can retry after invalidateNomadMicronWasmPreload", async () => {
+ globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__ = true;
+ globalThis.Go = class {
+ constructor() {
+ this.importObject = {};
+ this.run = vi.fn();
+ }
+ };
+
+ const appendSpy = vi.spyOn(document.head, "appendChild").mockImplementation((node) => {
+ if (node?.tagName === "SCRIPT" && typeof node.onload === "function") {
+ queueMicrotask(() => node.onload());
+ }
+ return node;
+ });
+
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: true,
+ arrayBuffer: async () => new ArrayBuffer(16),
+ headers: new Headers({ "content-type": "application/wasm" }),
+ });
+
+ vi.spyOn(WebAssembly, "instantiateStreaming").mockRejectedValue(new Error("streaming failed"));
+ const instantiate = vi
+ .spyOn(WebAssembly, "instantiate")
+ .mockRejectedValueOnce(new Error("first"))
+ .mockRejectedValueOnce(new Error("second"));
+
+ try {
+ expect(await preloadNomadMicronWasm()).toBe(false);
+ invalidateNomadMicronWasmPreload();
+ expect(await preloadNomadMicronWasm()).toBe(false);
+ expect(instantiate).toHaveBeenCalledTimes(2);
+ } finally {
+ appendSpy.mockRestore();
+ }
+ });
+
+ it("preloadNomadMicronWasm resolves true when micronConvert is already defined", async () => {
+ globalThis.__MESHCHATX_TEST_MICRON_WASM_BUNDLED__ = true;
+ globalThis.micronConvert = vi.fn(() => "");
+ expect(await preloadNomadMicronWasm()).toBe(true);
+ });
+});

diff --git a/tests/frontend/fixtures/settingsPageTestApi.js b/tests/frontend/fixtures/settingsPageTestApi.js
index 0e7f66a7..a3167b01 100644
--- a/tests/frontend/fixtures/settingsPageTestApi.js
+++ b/tests/frontend/fixtures/settingsPageTestApi.js
@@ -123,6 +123,7 @@ export function buildFullServerConfig(overrides = {}) {
nomad_render_markdown_enabled: true,
nomad_render_html_enabled: true,
nomad_render_plaintext_enabled: true,
+ nomad_micron_wasm_enabled: true,
nomad_default_page_path: "/page/index.mu",
gitea_base_url: "https://git.quad4.io",
...overrides,

diff --git a/vite.config.js b/vite.config.js
index 20309ab4..ceab4aa1 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,6 +1,7 @@
import path from "path";
import fs from "fs";
import { defineConfig } from "vite";
+import { MICRON_PARSER_GO_RELEASE_TAG } from "./scripts/micron-parser-go-version.mjs";
import tailwindcss from "@tailwindcss/vite";
import vue from "@vitejs/plugin-vue";
import vuetify from "vite-plugin-vuetify";
@@ -34,9 +35,27 @@ const e2eBackendWs = `ws://127.0.0.1:${e2eBackendPort}`;
const appBuildTimeIso = new Date().toISOString();
+function isMicronWasmBundledResolved() {
+ const wasmDir = path.join(__dirname, "meshchatx", "src", "frontend", "public", "vendor", "micron-parser-go");
+ const wasmFile = path.join(wasmDir, "micron-parser-go.wasm");
+ const execFile = path.join(wasmDir, "wasm_exec.js");
+ try {
+ if (!fs.existsSync(wasmFile) || !fs.existsSync(execFile)) {
+ return false;
+ }
+ return fs.statSync(wasmFile).size >= 8192 && fs.statSync(execFile).size >= 1024;
+ } catch {
+ return false;
+ }
+}
+
+const micronWasmBundled = isMicronWasmBundledResolved();
+
export default defineConfig({
define: {
__APP_BUILD_TIME__: JSON.stringify(appBuildTimeIso),
+ "import.meta.env.VITE_MICRON_WASM_BUNDLED": JSON.stringify(micronWasmBundled ? "true" : "false"),
+ "import.meta.env.VITE_MICRON_PARSER_GO_RELEASE": JSON.stringify(MICRON_PARSER_GO_RELEASE_TAG),
},
plugins: [
tailwindcss(),

diff --git a/vitest.config.js b/vitest.config.js
index b7591d04..973f6d55 100644
--- a/vitest.config.js
+++ b/vitest.config.js
@@ -1,12 +1,32 @@
import { defineConfig } from "vitest/config";
import vue from "@vitejs/plugin-vue";
import path from "path";
+import fs from "fs";
+import { MICRON_PARSER_GO_RELEASE_TAG } from "./scripts/micron-parser-go-version.mjs";
+
+function isMicronWasmBundledResolved(repoRoot) {
+ const wasmDir = path.join(repoRoot, "meshchatx", "src", "frontend", "public", "vendor", "micron-parser-go");
+ const wasmFile = path.join(wasmDir, "micron-parser-go.wasm");
+ const execFile = path.join(wasmDir, "wasm_exec.js");
+ try {
+ if (!fs.existsSync(wasmFile) || !fs.existsSync(execFile)) {
+ return false;
+ }
+ return fs.statSync(wasmFile).size >= 8192 && fs.statSync(execFile).size >= 1024;
+ } catch {
+ return false;
+ }
+}
+
+const micronWasmBundled = isMicronWasmBundledResolved(__dirname);
const appBuildTimeIso = new Date().toISOString();
export default defineConfig({
define: {
__APP_BUILD_TIME__: JSON.stringify(appBuildTimeIso),
+ "import.meta.env.VITE_MICRON_WASM_BUNDLED": JSON.stringify(micronWasmBundled ? "true" : "false"),
+ "import.meta.env.VITE_MICRON_PARSER_GO_RELEASE": JSON.stringify(MICRON_PARSER_GO_RELEASE_TAG),
},
plugins: [
vue({


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────